// AquosHardware.cs
//
// Implements HomeUX.Components.AquosHardware.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Controls a Sharp Aquos LC-52D65U, LC-42D65U, or LC-46D65U LCD TV connected via a serial port.
/// Compared to <r>AquosDriver</r>, <n>AquosHardware</n> provides a lower-level hardware-specific
/// interface.
/// </summary>
///
/// <remarks>
///
/// <para>Only a subset of the full functionality of the hardware can be accessed by this class.
/// </para>
///
/// <para>General usage pattern:</para>
///
/// <list type="number">
/// 	<item><description>Construct an instance of this class.</description></item>
///		<item><description>Hook up events.</description></item>
///		<item><description>Call <s>SimpleSerialHardware{}.Open</s> (inherited from
///			<r>SimpleSerialHardware{}</r>).</description></item>
///		<item><description>To control the hardware, you can use
/// 		<s>SimpleSerialHardware{}.TransmitCommand</s> (inherited from
///			<r>SimpleSerialHardware{}</r>) to send a raw command to the serial port, but in general
///			it's easier to use higher-level control methods like <r>SetVolume</r> and
/// 		<r>SetIsMuted</r>.  These methods construct and send a serial command and, when
/// 		the reply is received, they parse the hardware's reply and call a callback method you
/// 		provide, providing the parsed reply object as an argument.</description></item>
/// </list>
///
/// <para>Philosophy about exceptions:</para>
///
/// <list type="bullet">
///		<item><description>Programmer errors can lead to exceptions like <n>ArgumentException</n>.
/// 		</description></item>
///		<item><description>Hardware problems (including timeouts) won't lead to exceptions, but
///			rather will cause the following to occur:
///			<list type="bullet">
///				<item><description>The callback method you provided (in the "onComplete" parameter)
///					will be called with <n>null</n> in place of the parsed reply object.  This lets
/// 				you clean up your state.</description></item>
///				<item><description>Events such as <r>CommandFailed</r> and <r>WarningsReported</r>
///					will be fired as appropriate.  This lets you centralize reporting of hardware
///					problems to the user.</description></item>
///			</list>
///			</description></item>
///	</list>
///
/// </remarks>
///
[DebuggerDisplay("AquosHardware {HardwareLabel}")]
public class AquosHardware : SimpleSerialHardware<GenericReply>
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The message to display when the hardware sends something unexpected via the serial port.
    /// </summary>
    const string UnexpectedOutputMessage = "Unexpected output from hardware: {0}";

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    /// <summary>
    /// Fired when a command fails.
    /// </summary>
    public event EventHandler<AquosHardwareCommandFailedEventArgs> CommandFailed;

    /// <summary>
    /// Fired when this class has warning(s) to report to the application.  These warnings can be
	/// displayed to the user.
    /// </summary>
    public event EventHandler<AquosHardwareWarningsReportedEventArgs> WarningsReported;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods & Related Private Methods
    //

    /// <summary>
    /// Initializes an instance of this class.
    /// </summary>
    ///
    /// <param name="workQueue">The <r>WorkQueue</r> to use for scheduling asynchronous tasks.
	/// 	</param>
    ///
    /// <param name="serialPort">The serial port (real or virtual) to connect to.</param>
    ///
    /// <param name="hardwareLabel">A label to use for referring to this hardware device within
	/// 	warnings, trace output, etc.</param>
    ///
    public AquosHardware(WorkQueue workQueue, VirtualSerialPort serialPort, string hardwareLabel) :
        base(workQueue, serialPort, SimpleSerialHardwareMode.Line, hardwareLabel)
    {
        serialPort.NewLine = "\r";
    }

    /// <summary>
	/// Powers the TV on or off.
    /// </summary>
    ///
    /// <param name="isPowered"><n>true</n> to turn on the TV, <n>false</n> to turn it off.</param>
    ///
    /// <param name="onComplete">Called on completion of the operation.</param>
    ///
    public void SetIsPowered(bool isPowered, Action<GenericReply> onComplete)
    {
        string command = String.Format(CultureInfo.InvariantCulture,
            "POWR{0}   ", (isPowered ? "1" : "0"));
        //TransmitCommandThenPause(command, 3000, onComplete);
        TransmitCommand(command, onComplete);
    }

    /// <summary>
	/// Switches to a specified input.
    /// </summary>
    ///
    /// <param name="input">The input number to switch to (1-based).</param>
    ///
    /// <param name="onComplete">Called on completion of the operation.</param>
    ///
    public void SetInput(int input, Action<GenericReply> onComplete)
    {
        string command = String.Format(CultureInfo.InvariantCulture,
            "IAVD{0}   ", input);
        //TransmitCommandThenPause(command, 3000, onComplete);
        TransmitCommand(command, onComplete);
    }

    /// <summary>
	/// Sets the TV volume.
    /// </summary>
    ///
    /// <param name="volume">The volume to set the TV to, in the range 0 to 60 (inclusive).
    ///     </param>
    ///
    /// <param name="onComplete">Called on completion of the operation.</param>
    ///
    public void SetVolume(int volume, Action<GenericReply> onComplete)
    {
        string command = String.Format(CultureInfo.InvariantCulture,
            "VOLM{0:00}  ", volume);
        //TransmitCommandThenPause(command, 3000, onComplete);
        TransmitCommand(command, onComplete);
    }

    /// <summary>
	/// Mutes or unmutes the TV.
    /// </summary>
    ///
    /// <param name="isMuted"><n>true</n> to mute the TV, <n>false</n> to unmute it.</param>
    ///
    /// <param name="onComplete">Called on completion of the operation.</param>
    ///
    public void SetIsMuted(bool isMuted, Action<GenericReply> onComplete)
    {
        string command = String.Format(CultureInfo.InvariantCulture,
            "MUTE{0}   ", (isMuted ? "1" : "0"));
		//TransmitCommandThenPause(command, 3000, onComplete);
        TransmitCommand(command, onComplete);
    }

#warning this isn't working yet:
    /// <summary>
    /// Transmits a given command to the hardware, waits for the reply from the hardware, then,
    /// on successful reply, pauses for a given number of milliseconds.
    /// </summary>
    ///
    /// <param name="command">The command to send to the hardware.</param>
    ///
    /// <param name="pauseMilliseconds">How long to pause, in milliseconds.</param>
    ///
    /// <param name="onComplete">Called on completion of the operation.</param>
    ///
    void TransmitCommandThenPause(string command, int pauseMilliseconds,
		Action<GenericReply> onComplete)
	{
        TransmitCommand(command, delegate(GenericReply reply)
        {
            if (reply == null) // e.g. hardware reported "ERR"
                onComplete(null);
            Util.Trace("{0}: Pausing after {1}", HardwareLabel, command);
            WorkQueue.Add(5000, delegate
            {
                onComplete(GenericReply.Reply);
            });
        });
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // SimpleSerialHardware Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>SimpleSerialHardware{}.ParseReceivedString</r>.
    /// </summary>
    ///
    /// <param name="received">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="pushBack">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="currentCommand">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="onReply">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    ///
    protected override void ParseReceivedString(string received, ref string pushBack,
        string currentCommand, Action<GenericReply> onReply)
    {
		// The Aquos hardware responds to command by sending either "OK\r" or "ERR\r" -- anything
		// else is unexpected output...

        // occasionally the Aquos sends null characters -- ignore them
        received = received.Replace("\0", String.Empty);

        // <commandError> will be set to true if the hardware reported an error with the command
        bool commandError = false;

        // <commandSuccess> will be set to true if the hardware reported that the command
        // succeeded
        bool commandSuccess = false;

        // if we come across miscellaneous issue(s) to report (e.g. the driver sends text we
        // don't expect), <warnings> will be set to the issue(s) to report, otherwise it
        // will be null
        LazyStringList warnings = new LazyStringList();

        // if this driver previously sent a command to the hardware, look for the reply now
        if (currentCommand != null)
        {
            // search the received string for the first occurrence of "OK\r" (indicating success)
            // or "ERR\r" (indicating failure); if found, set <commandSuccess> or <commandError>
			// accordingly, set <index> to the index of the found string, and set <find> to the
			// string that was searched for
            string find = "OK\r";
            int index = received.IndexOf(find);
            if (index >= 0)
				commandSuccess = true;
			else
			{
				find = "ERR\r";
				index = received.IndexOf(find);
				if (index >= 0)
					commandError = true;
			}

			// if either "OK\r" or "ERR\r" were found, check for anything before or after that
			// string -- any such text is reported as unexpected hardware output and otherwise
			// ignored -- and, in either case, end the command
			if (commandSuccess || commandError)
			{
                // everything before <find> is reported as unexpected hardware output and ignored
                if (index > 0)
                {
                    warnings.Add(String.Format(UnexpectedOutputMessage,
                        received.Substring(0, index)));
                }

                // everything after <find> is reported as unexpected hardware output and ignored
                int findEndIndex = index + find.Length;
                if (findEndIndex < received.Length)
                {
                    warnings.Add(String.Format(UnexpectedOutputMessage,
                        received.Substring(findEndIndex)));
                }

                // update driver state to indicate that we're no longer executing a command
                EndCommand();
            }
            else
            {
				// we haven't found the echoed command yet -- we return the received
				// string to <pushBack>
				pushBack = received + pushBack;
            }
        }

        // We've finished processing the text received from the hardware, and we've updated driver
        // serial communication state (i.e. <pushBack>) and filled in these variables:
        //   -- <commandError> = true if the driver reported an error ("ERR").
        //   -- <commandSuccess> = true if the driver reported success ("OK").
        //   -- <warnings> = issues to report to the user.
        //
        // Note that if a reply from a hardware command was fully received, <currentCommand> will
		// be null at this point, indicating that the hardware is ready to accept a new command.
        //

        // report a failed command, if that happened
        if (commandError)
        {
            CommandFailed(this, new AquosHardwareCommandFailedEventArgs()
            {
                Command = currentCommand
            });
        }

        // report warnings, if any
        ReportWarnings(warnings);

        // if there was a pending command that completed, call the command's callback function
        if (commandError)
            onReply(null);
        else
        if (commandSuccess)
            onReply(GenericReply.Reply);
    }

    /// <summary>
    /// Fires <r>WarningsReported</r> as needed to inform the application about warnings in a given
	/// list.
    /// </summary>
    ///
    /// <param name="warnings">The warnings, if any.</param>
    ///
	public void ReportWarnings(LazyStringList warnings)
	{
        if ((warnings.Count > 0) && (WarningsReported != null))
        {
            WarningsReported(this, new AquosHardwareWarningsReportedEventArgs()
            {
                Messages = warnings.ListOrNull
            });
        }
	}
}

/// <summary>
/// Arguments of the <r>AquosHardware.CommandFailed</r> event.
/// </summary>
///
[DebuggerDisplay("Command = {Command}")]
public class AquosHardwareCommandFailedEventArgs : EventArgs
{
    /// <summary>
    /// Gets the command that failed.
    /// </summary>
    public string Command { get; internal set; }
}

/// <summary>
/// Arguments of the <r>AquosHardware.WarningsReported</r> event.
/// </summary>
///
[DebuggerDisplay("Count = {Messages.Count}")]
public class AquosHardwareWarningsReportedEventArgs : EventArgs
{
    /// <summary>
    /// Gets the warnings that were reported.
    /// </summary>
    public ICollection<string> Messages { get; internal set; }
}

}
